#include "StdAfx.h"
#include "PoomPictures.h"
#include "PoomPictureLoader.h"
#include "CommonWM.h"
#include "PictureSaver.h"
#include "Settings.h"
#include "PoomData.h"

#ifdef _DEBUG
#include "crtdbg.h"
#endif

#define MAX_THREAD_WAIT_TIME     20000
#define POOM_PICTURES_CACHE_DIR  TEXT("iContact")
#define POOM_PICTURES_CACHE_FILE TEXT("iContactPics.cache")
#define EVT_POOMPICTURESACTION   TEXT("PoomPicturesAction")

PoomPictureList PoomPictures::pictureList;
bool PoomPictures::isRunning = false;
bool PoomPictures::isTerminating = false;
bool PoomPictures::isBusy = false;
HANDLE PoomPictures::hThread = NULL;
DWORD PoomPictures::dwThread = 0;
HANDLE PoomPictures::evtAction = NULL;
HWND PoomPictures::hWnd = NULL;
bool PoomPictures::sendNotify = false;
bool PoomPictures::delayLoading = false;
bool PoomPictures::enableCaching = false;

CRITICAL_SECTION PoomPictures::csSync;

bool PoomPictures::Initialize(HWND hWnd, bool sendNotifications, bool delayLoading, bool enableCaching)
{
  if (isRunning || hWnd == NULL)
    return false;

  PoomPictures::hWnd = hWnd;
  PoomPictures::sendNotify = sendNotifications;
  PoomPictures::delayLoading = delayLoading;
  PoomPictures::enableCaching = enableCaching;

  evtAction = CreateEvent(NULL, FALSE, FALSE, EVT_POOMPICTURESACTION);
  if (evtAction == NULL)
    return false;

  InitializeCriticalSection(&csSync);

  hThread = CreateThread(NULL, 0, PoomPictures::MainThread, NULL, 0, &dwThread);
  if (hThread == NULL)
  {
    return false;
  }

  isRunning = true;
  return true;
}

void PoomPictures::Terminate()
{
  if (!isRunning)
    return;

  SyncTerminate(true);

  WaitForSingleObject(hThread, MAX_THREAD_WAIT_TIME);

  EnterCriticalSection(&csSync);
  pictureList.Clear();
  LeaveCriticalSection(&csSync);

  DeleteCriticalSection(&csSync);

  isRunning = false;
  isTerminating = false;
  isBusy = false;
  hThread = NULL;
  dwThread = 0;
  hWnd = NULL;
}

bool PoomPictures::AssignPicture(CEOID oId, const TCHAR* szFile)
{
  if (!Settings::picSaveEnabled || !szFile || *szFile == 0)
    return false;

  if (oId == 0)
    return false;

  bool result = true;

  if (Settings::picSaveToFile)
  {
    // get File As
    TCHAR szFileAs[128] = {0};
    IContact* pContact = NULL;
    if (SUCCEEDED(PoomData::GetApp()->GetItemFromOid(oId, reinterpret_cast<IDispatch**>(&pContact))))
    {
      BSTR bstr = NULL;
      if (SUCCEEDED(pContact->get_FileAs(&bstr)))
      {
        ::StringCchCopy(szFileAs, 128, bstr);
      }
      SysFreeString(bstr);
      pContact->Release();
    }

    if (*szFileAs != 0)
    {
      IImage* pSrcImage = NULL;
      if (PictureSaver::LoadImageFromFile(&pSrcImage, szFile))
      {
        // resize to screen size
        int width  = ::GetSystemMetrics(SM_CXSCREEN);
        int height = ::GetSystemMetrics(SM_CYSCREEN);
        IImage* pImage = NULL;
        if (!PictureSaver::ResizeImage(pSrcImage, &pImage, width, height))
        {
          pImage = pSrcImage;
        }

        EImageFormat fmt = PictureSaver::GetImageFormat(szFile);
        if (fmt == ifNone)
          fmt = ifJpeg;

        TCHAR szDstFile[MAX_PATH*2];
        ::StringCchCopy(szDstFile, MAX_PATH*2, Settings::picSaveFolder);

        if (szDstFile[::_tcslen(szDstFile) - 1] != TEXT('\\'))
          ::StringCchCat(szDstFile, MAX_PATH*2, TEXT("\\"));

        ::StringCchCat(szDstFile, MAX_PATH*2, szFileAs);
        ::StringCchCat(szDstFile, MAX_PATH*2, PictureSaver::GetFormatExt(fmt));

        result &= PictureSaver::SaveImageToFile(pImage, szDstFile, fmt);

        pImage->Release();
      }
    }
  }

  if (result && Settings::picSaveToOutlook)
  {
    result &= PoomPictureLoader::SetContactPicture(PoomData::GetApp(), oId, szFile, Settings::picSaveOutlookType);
  }

  return result;
}

PoomPicture* PoomPictures::Add(CEOID oid, int width, int height, bool reset)
{
  if (!isRunning)
    return NULL;

  PoomPicture* pic = pictureList.Find(oid);
  if (pic)
  {
    if (!pic->isActive)
    {
      EnterCriticalSection(&csSync);
      pic->isActive = true;
      LeaveCriticalSection(&csSync);
    }
    if (reset || !pic->isLoaded)
    {
      EnterCriticalSection(&csSync);
      pic->isLoaded = false;
      pic->width = width;
      pic->height = height;
      LeaveCriticalSection(&csSync);
      if (reset || !PoomPictures::delayLoading)
        SetEvent(evtAction);
    }
  }
  else
  {
    EnterCriticalSection(&csSync);
    pic = pictureList.Add(oid, true, false, false, NULL, width, height);
    LeaveCriticalSection(&csSync);
    if (pic)
    {
      if (reset || !PoomPictures::delayLoading)
        SetEvent(evtAction);
    }
  }

  return pic;
}

void PoomPictures::BeginLoading()
{
  if (SyncHasTasks())
    SetEvent(evtAction);
  delayLoading = false;
}

void PoomPictures::Remove(CEOID oid)
{
  if (!isRunning)
    return;

  PoomPicture* pic = pictureList.Find(oid);
  if (pic != NULL)
  {
    EnterCriticalSection(&csSync);
    pictureList.Remove(oid);
    LeaveCriticalSection(&csSync);
  }
}

void PoomPictures::SyncTerminate(bool terminate)
{
  EnterCriticalSection(&csSync);
  isTerminating = terminate;
  LeaveCriticalSection(&csSync);
  SetEvent(evtAction);
}

bool PoomPictures::SyncIsTerminating()
{
  bool terminating;
  EnterCriticalSection(&csSync);
  terminating = isTerminating;
  LeaveCriticalSection(&csSync);
  return terminating;
}

void PoomPictures::SyncSetBusy(bool busy)
{
  ::EnterCriticalSection(&csSync);
  isBusy = busy;
  ::LeaveCriticalSection(&csSync);
  ::PostMessage(hWnd, NM_THREAD_BUSY, 0, busy ? TRUE : FALSE);
}

bool PoomPictures::SyncHasTasks()
{
  bool haveLoadTasks = false;

  PoomPictureListIter iter(PoomPictures::pictureList);
  EnterCriticalSection(&csSync);
  while(iter.hasData())
  {
    if (iter->isActive && !iter->isLoaded)
    {
      haveLoadTasks = true;
      break;
    }
    iter.next();
  }
  LeaveCriticalSection(&csSync);

  return haveLoadTasks;
}

bool PoomPictures::SyncGetNextItemToLoad(CEOID& oid, int& width, int& height)
{
  bool result = false;
  PoomPictureListIter iter(PoomPictures::pictureList);

  EnterCriticalSection(&csSync);
  while(iter.hasData())
  {
    if (iter->isActive && !iter->isLoaded)
    {
      oid = iter->oid;
      width = iter->width;
      height = iter->height;
      result = true;
      break;
    }
    iter.next();
  }
  LeaveCriticalSection(&csSync);

  return result;
}

bool PoomPictures::SyncAssignItemPicture(CEOID oid, HBITMAP hbm, int width, int height)
{
  bool result = false;
  PoomPicture* item = NULL;

  EnterCriticalSection(&csSync);
  item = PoomPictures::pictureList.Find(oid);
  if (item != NULL)
  {
    if (item->oid == oid)
    {
      if (item->hBitmap != NULL)
        DeleteObject(item->hBitmap);
      item->hBitmap = hbm;
      item->width = width;
      item->height = height;
      item->isLoaded = true;
      item->isLoadedFromCache = false;

      if (item->hBitmap != NULL && PoomPictures::sendNotify)
        PostMessage(PoomPictures::hWnd, NM_POOM_PICTURE_LOADED, (WPARAM)oid, 0);

      result = true;
    }
  }
  LeaveCriticalSection(&csSync);

  return result;
}

void PoomPictures::SyncSendLoadNotify(CEOID oid)
{
  if (PoomPictures::sendNotify)
    PostMessage(PoomPictures::hWnd, NM_POOM_PICTURE_LOADED, (WPARAM)oid, 0);
}

DWORD PoomPictures::MainThread(LPVOID lpInstance)
{
  HANDLE evtAction = OpenEvent(EVENT_ALL_ACCESS, FALSE, EVT_POOMPICTURESACTION);
  if (evtAction == NULL)
  {
    return 1;
  }

  IPOutlookApp2* polApp = NULL;

  if (FAILED(CoCreateInstance(CLSID_Application, NULL, CLSCTX_INPROC_SERVER, 
                IID_IPOutlookApp2, reinterpret_cast<void **>(&polApp))))
  {
    return 1;
  }

  // login to the Pocket Outlook object model
  if(FAILED(polApp->Logon(NULL)))
  {
    polApp->Release();
    return 1;
  }

  // load cache
  if (PoomPictures::enableCaching)
  {
    if (PoomPictures::delayLoading)
    {
      WaitForSingleObject(evtAction, INFINITE);
      if (PoomPictures::SyncHasTasks())
      {
        SetEvent(evtAction);
      }
    }
    SyncSetBusy(true);
    LoadCache();
    SyncSetBusy(false);
  }

  HDC windowDC = GetDC(PoomPictures::hWnd);

  while (!PoomPictures::SyncIsTerminating())
  {
    WaitForSingleObject(evtAction, INFINITE);

    if (PoomPictures::SyncIsTerminating())
    {
      break;
    }

    CEOID oid;
    HBITMAP hBitmap;
    int width;
    int height;

    SyncSetBusy(true);

    while (PoomPictures::SyncGetNextItemToLoad(oid, width, height))
    {
      if (PoomPictures::SyncIsTerminating())
      {
        break;
      }

      if (PoomPictureLoader::GetContactPicture(polApp, oid, windowDC, hBitmap, width, height))
      {
        if (!PoomPictures::SyncAssignItemPicture(oid, hBitmap, width, height))
          ::DeleteObject(hBitmap);
      }
      else
      { // contact doesn't have a picture
        PoomPictures::SyncAssignItemPicture(oid, NULL, 0, 0);
      }
    }

    SyncSetBusy(false);
  }

  ReleaseDC(PoomPictures::hWnd, windowDC);

  if (PoomPictures::enableCaching)
  {
    SyncSetBusy(true);
    SaveCache();
    SyncSetBusy(false);
  }

  polApp->Logoff();
  polApp->Release();

  return 0;
}

void PoomPictures::GetCacheFileName(TCHAR* fileName)
{
  if (SHGetSpecialFolderPath(NULL, fileName, CSIDL_APPDATA, FALSE) == FALSE)
  {
    StringCchCopy(fileName, MAX_PATH, TEXT("\\Application Data"));
  }
  StringCchCat(fileName, MAX_PATH, TEXT("\\"));
  StringCchCat(fileName, MAX_PATH, POOM_PICTURES_CACHE_DIR);
  StringCchCat(fileName, MAX_PATH, TEXT("\\"));
  StringCchCat(fileName, MAX_PATH, POOM_PICTURES_CACHE_FILE);
}

void PoomPictures::LoadCache()
{
  TCHAR strFile[MAX_PATH];
  GetCacheFileName(strFile);

  HANDLE hFile = CreateFile(strFile, GENERIC_READ, FILE_SHARE_READ, NULL,
                            OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hFile == INVALID_HANDLE_VALUE)
    return;

  DWORD nPictures = 0;
  DWORD nBytesRead = 0;
  if (ReadFile(hFile, &nPictures, sizeof(DWORD), &nBytesRead, NULL) == FALSE)
  {
    CloseHandle(hFile);
    return;
  }

  bool picturesLoaded = false;

  HDC hdc = GetDC(PoomPictures::hWnd);
  HDC hdcCache1 = CreateCompatibleDC(hdc);
  HDC hdcCache2 = CreateCompatibleDC(hdc);

  for (DWORD i = 0; i < nPictures; i++)
  {
    if (PoomPictures::SyncIsTerminating())
      break;

    CEOID   oid;
    HBITMAP hBitmap;
    int     width;
    int     height;
    if (LoadBitmap(hFile, hdc, hdcCache1, hdcCache2, oid, hBitmap, width, height))
    {
      EnterCriticalSection(&csSync);
      PoomPicture* picture = pictureList.Find(oid);
      if (picture == NULL)
      {
        pictureList.Add(oid, false, true, true, hBitmap, width, height);
      }
      else
      {
        picture->isLoaded = true;
        picture->isLoadedFromCache = true;
        picture->hBitmap = hBitmap;
        picture->width = width;
        picture->height = height;
      }
      LeaveCriticalSection(&csSync);
      picturesLoaded = true;
    }
  }

  DeleteDC(hdcCache2);
  DeleteDC(hdcCache1);
  ReleaseDC(PoomPictures::hWnd, hdc);

  CloseHandle(hFile);

  if (picturesLoaded)
    PoomPictures::SyncSendLoadNotify(0);
}

void PoomPictures::SaveCache()
{
  TCHAR strFile[MAX_PATH];
  GetCacheFileName(strFile);

  HANDLE hCacheFile = CreateFile(strFile, GENERIC_WRITE, NULL, NULL,
                          CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hCacheFile == INVALID_HANDLE_VALUE)
  {
    if (SHGetSpecialFolderPath(NULL, strFile, CSIDL_APPDATA, FALSE) == FALSE)
    {
      StringCchCopy(strFile, MAX_PATH, TEXT("\\Application Data"));
    }
    StringCchCat(strFile, MAX_PATH, TEXT("\\"));
    StringCchCat(strFile, MAX_PATH, POOM_PICTURES_CACHE_DIR);
    CreateDirectory(strFile, NULL);
    StringCchCat(strFile, MAX_PATH, TEXT("\\"));
    StringCchCat(strFile, MAX_PATH, POOM_PICTURES_CACHE_FILE);

    hCacheFile = CreateFile(strFile, GENERIC_WRITE, NULL, NULL,
                            CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hCacheFile == INVALID_HANDLE_VALUE)
      hCacheFile = NULL;
  }

  DWORD numPictures = 0;
  EnterCriticalSection(&csSync);
  numPictures = PoomPictures::pictureList.GetCount();
  LeaveCriticalSection(&csSync);

  DWORD numBytes;
  WriteFile(hCacheFile, (void*)&numPictures, sizeof(DWORD), &numBytes, NULL);

  if (numPictures > 0)
  {
    // create HDCs to speed up the saving
    HDC windowDC = GetDC(PoomPictures::hWnd);
    HDC hdcCache1 = CreateCompatibleDC(windowDC);
    HDC hdcCache2 = CreateCompatibleDC(windowDC);

    EnterCriticalSection(&csSync);
    PoomPictureListIter iter(PoomPictures::pictureList);
    while (iter.hasData())
    {
      if (iter->isActive && iter->hBitmap != NULL)
        SaveBitmap(hCacheFile, windowDC, hdcCache1, hdcCache2, iter->oid, iter->hBitmap);
      iter.next();
    }
    LeaveCriticalSection(&csSync);

    DeleteDC(hdcCache2);
    DeleteDC(hdcCache1);
    ReleaseDC(PoomPictures::hWnd, windowDC);
  }

  CloseHandle(hCacheFile);
}

bool PoomPictures::LoadBitmap(HANDLE hFile, HDC hdc, CEOID& oid, HBITMAP& hBitmap, int& width, int& height)
{
  HDC hdc1 = CreateCompatibleDC(hdc);
  HDC hdc2 = CreateCompatibleDC(hdc);
  bool res = LoadBitmap(hFile, hdc, hdc1, hdc2, oid, hBitmap, width, height);
  DeleteDC(hdc2);
  DeleteDC(hdc1);
  return res;
}

bool PoomPictures::LoadBitmap(HANDLE hFile, HDC hdc, HDC hdc1, HDC hdc2, CEOID& oid, HBITMAP& hBitmap, int& width, int& height)
{
  // Load header
  DWORD numBytes;
  if (!ReadFile(hFile, &oid, sizeof(CEOID), &numBytes, NULL))
    return false;
  if (!ReadFile(hFile, &width, sizeof(int), &numBytes, NULL))
    return false;
  if (!ReadFile(hFile, &height, sizeof(int), &numBytes, NULL))
    return false;
  WORD bitsPixel = 0;
  if (!ReadFile(hFile, &bitsPixel, sizeof(WORD), &numBytes, NULL))
    return false;

  if (width <= 0 || height <= 0)
    return false;

  // Prepare header
  HBITMAP hDib = NULL;
  LPBYTE  pActualBits = NULL;
  BITMAPINFO dibInfo;
  ZeroMemory(&dibInfo, sizeof(BITMAPINFO));
  dibInfo.bmiHeader.biSize          = sizeof(BITMAPINFO);
  dibInfo.bmiHeader.biWidth         = width;
  dibInfo.bmiHeader.biHeight        = height;
  dibInfo.bmiHeader.biBitCount      = bitsPixel;
  dibInfo.bmiHeader.biPlanes        = 1;
  dibInfo.bmiHeader.biSizeImage     = width * height * bitsPixel / 8;
  dibInfo.bmiHeader.biXPelsPerMeter = 3780; // 96 ppi
  dibInfo.bmiHeader.biYPelsPerMeter = 3780;

  // This will return ptr to actual DIB bits in pBits
  hDib = CreateDIBSection(hdc, (BITMAPINFO*)&dibInfo, DIB_RGB_COLORS, (void**)&pActualBits, NULL, 0);
  if (hDib == NULL || pActualBits == NULL)
  {
    return false;
  }

  // Load bits
  if (ReadFile(hFile, (void*)pActualBits, dibInfo.bmiHeader.biSizeImage, &numBytes, NULL) == FALSE)
  {
    DeleteObject(hDib);
    return false;
  }

  // Create bitmap
  hBitmap = CreateCompatibleBitmap(hdc, width, height);

  // Grab bitmap
  HBITMAP hOldBitmap1 = (HBITMAP)SelectObject(hdc1, hBitmap);
  HBITMAP hOldBitmap2 = (HBITMAP)SelectObject(hdc2, hDib);
  BitBlt(hdc1, 0, 0, width, height, hdc2, 0, 0, SRCCOPY);
  SelectObject(hdc2, hOldBitmap2);
  SelectObject(hdc1, hOldBitmap1);
  DeleteObject(hDib);

  return true;
}

bool PoomPictures::SaveBitmap(HANDLE hFile, HDC hdc, HDC hdc1, HDC hdc2, CEOID oid, HBITMAP hBitmap)
{
  BITMAP bmpInfo;
  GetObject(hBitmap, sizeof(bmpInfo), (LPVOID)&bmpInfo);

  // prepare header
  HBITMAP hDib = NULL;
  LPBYTE  pBits = NULL;
  BITMAPINFO dibInfo;
  ZeroMemory(&dibInfo, sizeof(BITMAPINFO));
  dibInfo.bmiHeader.biSize          = sizeof(BITMAPINFO);
  dibInfo.bmiHeader.biWidth         = bmpInfo.bmWidth;
  dibInfo.bmiHeader.biHeight        = bmpInfo.bmHeight;
  dibInfo.bmiHeader.biBitCount      = bmpInfo.bmBitsPixel;
  dibInfo.bmiHeader.biPlanes        = bmpInfo.bmPlanes;
  dibInfo.bmiHeader.biSizeImage     = bmpInfo.bmWidth * bmpInfo.bmHeight * bmpInfo.bmBitsPixel / 8;
  dibInfo.bmiHeader.biXPelsPerMeter = 3780; // 96 ppi
  dibInfo.bmiHeader.biYPelsPerMeter = 3780;

  //This will return ptr to actual DIB bits in pBits
  hDib = CreateDIBSection(hdc, (BITMAPINFO*)&dibInfo, DIB_RGB_COLORS, (void**)&pBits, NULL, 0);
  if (hDib == NULL || pBits == NULL)
  {
    return false;
  }
  //Grab bitmap
  HBITMAP hOldBitmap1 = (HBITMAP)SelectObject(hdc1, hBitmap);
  HBITMAP hOldBitmap2 = (HBITMAP)SelectObject(hdc2, hDib);
  BitBlt(hdc2, 0, 0, bmpInfo.bmWidth, bmpInfo.bmHeight, hdc1, 0, 0, SRCCOPY);
  SelectObject(hdc2, hOldBitmap2);
  SelectObject(hdc1, hOldBitmap1);

  DWORD numBytes;
  // save oid
  if (!WriteFile(hFile, (void*)&oid, sizeof(CEOID), &numBytes, NULL))
  {
    DeleteObject(hDib);
    return false;
  }
  // save width, height and bpp
  if (!WriteFile(hFile, (void*)&bmpInfo.bmWidth, sizeof(LONG), &numBytes, NULL))
  {
    DeleteObject(hDib);
    return false;
  }
  if (!WriteFile(hFile, (void*)&bmpInfo.bmHeight, sizeof(LONG), &numBytes, NULL))
  {
    DeleteObject(hDib);
    return false;
  }
  if (!WriteFile(hFile, (void*)&bmpInfo.bmBitsPixel, sizeof(WORD), &numBytes, NULL))
  {
    DeleteObject(hDib);
    return false;
  }
  // save bits
  if (!WriteFile(hFile, (void*)pBits, dibInfo.bmiHeader.biSizeImage, &numBytes, NULL))
  {
    DeleteObject(hDib);
    return false;
  }

  return true;
}


//
// IMPLEMENTATION OF PoomPictureList
//

PoomPictureList::PoomPictureList()
{
  first = last = NULL;
  count = 0;
}

PoomPictureList::~PoomPictureList()
{
  Clear();
}

void PoomPictureList::Clear()
{
  while(first != NULL)
  {
    last = first;
    first = first->next;
    if (last->item)
    {
      if (last->item->hBitmap)
        ::DeleteObject(last->item->hBitmap);
      delete last->item;
    }
    delete last;
  }
  first = last = NULL;
  count = 0;
}

PoomPicture* PoomPictureList::Add(CEOID oid, bool active, bool loaded, bool fromCache, HBITMAP hbm, int width, int height)
{
  PoomPictureItem* newItem = new PoomPictureItem;
  if (!newItem)
    throw TEXT("Not enough memory");
  newItem->item = new PoomPicture();
  if (!newItem->item)
    throw TEXT("Not enough memory");
  newItem->next = NULL;

  newItem->item->oid = oid;
  newItem->item->isActive = active;
  newItem->item->isLoaded = loaded;
  newItem->item->isLoadedFromCache = fromCache;
  newItem->item->hBitmap = hbm;
  newItem->item->width = width;
  newItem->item->height = height;

  if (first == NULL)
  {
    first = newItem;
  }
  else
  {
    last->next = newItem;
  }
  last = newItem;

  count++;

  return newItem->item;
}

void PoomPictureList::Remove(CEOID oid)
{
  PoomPictureItem* prev = NULL;
  PoomPictureItem* curr = first;
  while (curr)
  {
    if (curr->item && curr->item->oid == oid)
    {
      if (prev)
      {
        prev->next = curr->next;
        if (last == curr)
          last = prev;
      }
      else
      {
        first = curr->next;
        if (last == curr)
          last = first;
      }
      if (curr->item)
      {
        if (curr->item->hBitmap != NULL)
          ::DeleteObject(curr->item->hBitmap);
        delete curr->item;
      }
      delete curr;

      count--;

      break;
    }
    prev = curr;
    curr = curr->next;
  }
}

PoomPicture* PoomPictureList::Find(CEOID oid)
{
  PoomPictureItem* curr = first;
  while (curr)
  {
    if (curr->item && curr->item->oid == oid)
    {
      return curr->item;
    }
    curr = curr->next;
  }
  return NULL;
}

long PoomPictureList::GetCount()
{
  return count;
}